/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include "pagespeed/system/admin_site.h"

#include <cstddef>
#include <memory>
#include <set>
#include <vector>

#include "net/instaweb/http/public/async_fetch.h"
#include "net/instaweb/http/public/http_cache.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/rewrite_query.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "net/instaweb/util/public/property_cache.h"
#include "net/instaweb/util/public/property_store.h"
//#include "strings/stringpiece_utils.h"
#include "pagespeed/kernel/base/cache_interface.h"
#include "pagespeed/kernel/base/callback.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/string_writer.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/kernel/cache/purge_context.h"
#include "pagespeed/kernel/html/html_keywords.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/query_params.h"
#include "pagespeed/kernel/http/request_headers.h"
#include "pagespeed/kernel/http/response_headers.h"
#include "pagespeed/kernel/util/statistics_logger.h"
#include "pagespeed/system/system_cache_path.h"
#include "pagespeed/system/system_caches.h"
#include "pagespeed/system/system_rewrite_options.h"

namespace net_instaweb {

// Generated from JS, CSS, and HTML source via net/instaweb/js/data_to_c.cc.
extern const char* CSS_admin_site_css;
extern const char* CSS_caches_css;
extern const char* CSS_console_css;
extern const char* CSS_graphs_css;
extern const char* CSS_statistics_css;
extern const char* JS_caches_js;
extern const char* JS_caches_js_opt;
extern const char* JS_console_js;
extern const char* JS_console_js_opt;
extern const char* JS_graphs_js;
extern const char* JS_graphs_js_opt;
extern const char* JS_messages_js;
extern const char* JS_messages_js_opt;
extern const char* JS_statistics_js;
extern const char* JS_statistics_js_opt;

namespace {

struct Tab {
  const char* label;
  const char* title;
  const char* admin_link;       // relative from /pagespeed_admin/
  const char* statistics_link;  // relative from /mod_pagespeed_statistics
  const char* space;            // html for inter-link spacing.
};

const char kShortBreak[] = " ";
const char kLongBreak[] = " &nbsp;&nbsp; ";

// TODO(jmarantz): disable or recolor links to pages that are not available
// based on the current config.
const Tab kTabs[] = {
    {"Statistics", "Statistics", "statistics", "?", kLongBreak},
    {"Configuration", "Configuration", "config", "?config", kShortBreak},
    {"Histograms", "Histograms", "histograms", "?histograms", kLongBreak},
    {"Caches", "Caches", "cache", "?cache", kLongBreak},
    {"Console", "Console", "console", nullptr, kLongBreak},
    {"Message History", "Message History", "message_history", nullptr,
     kLongBreak},
    {"Graphs", "Graphs", "graphs", nullptr, kLongBreak},
};

// Controls the generation of an HTML Admin page.  Constructing it
// establishes the content-type as HTML and response code 200, and
// puts in a banner with links to all the admin pages, ready for
// appending more <body> elements.  Destructing AdminHtml closes the
// body and completes the fetch.
class AdminHtml {
 public:
  AdminHtml(StringPiece current_link, StringPiece head_extra,
            AdminSite::AdminSource source, Timer* timer, AsyncFetch* fetch,
            MessageHandler* handler)
      : fetch_(fetch), handler_(handler) {
    fetch->response_headers()->SetStatusAndReason(HttpStatus::kOK);
    fetch->response_headers()->Add(HttpAttributes::kContentType, "text/html");

    int64 now_ms = timer->NowMs();
    fetch->response_headers()->SetLastModified(now_ms);

    // Let PageSpeed dynamically minify the html, css, and javasript
    // generated by the admin page, to the extent it's not done
    // already by the tools.  Note, this does mean that viewing the
    // statistics and histograms pages will affect the statistics and
    // histograms.  If we decide this is too annoying, then we can
    // instead procedurally minify the css/js and leave the html
    // alone.
    //
    // Note that we at least turn off add_instrumenation here by explicitly
    // giving a filter list without "+" or "-".
    fetch->response_headers()->Add(
        RewriteQuery::kPageSpeedFilters,
        "rewrite_css,rewrite_javascript,collapse_whitespace");

    // Generate some navigational links to help our users get to other
    // admin pages.
    fetch->Write("<!DOCTYPE html>\n<html><head>", handler_);
    fetch->Write(StrCat("<style>", CSS_admin_site_css, "</style>"), handler_);

    GoogleString buf;
    for (int i = 0, n = arraysize(kTabs); i < n; ++i) {
      const Tab& tab = kTabs[i];
      const char* link = nullptr;
      switch (source) {
        case AdminSite::kPageSpeedAdmin:
          link = tab.admin_link;
          break;
        case AdminSite::kStatistics:
          link = tab.statistics_link;
          break;
        case AdminSite::kOther:
          link = nullptr;
          break;
      }
      if (link != nullptr) {
        StringPiece style;
        if (tab.admin_link == current_link) {
          style = " style='color:darkblue;text-decoration:underline;'";
          fetch->Write(StrCat("<title>PageSpeed ", tab.title, "</title>"),
                       handler_);
        }
        StrAppend(&buf, "<a href='", link, "'", style, ">", tab.label, "</a>",
                  tab.space);
      }
    }

    fetch->Write(StrCat(head_extra, "</head>"), handler_);
    fetch->Write(StrCat("<body class='pagespeed-admin-body'>"
                        "<div class='pagespeed-admin-tabs'>\n"
                        "<b>Pagespeed Admin</b>",
                        kLongBreak, "\n"),
                 handler_);
    fetch->Write(buf, handler_);
    fetch->Write("</div><hr/>\n", handler_);
    fetch->Flush(handler_);
  }

  ~AdminHtml() {
    fetch_->Write("</body></html>", handler_);
    fetch_->Done(true);
  }

 private:
  AsyncFetch* fetch_;
  MessageHandler* handler_;
};

}  // namespace

AdminSite::AdminSite(StaticAssetManager* static_asset_manager, Timer* timer,
                     MessageHandler* message_handler)
    : message_handler_(message_handler),
      static_asset_manager_(static_asset_manager),
      timer_(timer) {}

// Handler which serves PSOL console.
void AdminSite::ConsoleHandler(const SystemRewriteOptions& global_options,
                               const RewriteOptions& options,
                               AdminSource source,
                               const QueryParams& query_params,
                               AsyncFetch* fetch, Statistics* statistics) {
  if (query_params.Has("json")) {
    ConsoleJsonHandler(query_params, fetch, statistics);
    return;
  }

  MessageHandler* handler = message_handler_;
  bool statistics_enabled = global_options.statistics_enabled();
  bool logging_enabled = global_options.statistics_logging_enabled();
  bool log_dir_set = !global_options.log_dir().empty();

  // TODO(jmarantz): change StaticAssetManager to take options by const ref.
  // TODO(sligocki): Move static content to a data2cc library.
  StringPiece console_js = options.Enabled(RewriteOptions::kDebug)
                               ? JS_console_js
                               : JS_console_js_opt;
  // TODO(sligocki): Do we want to have a minified version of console CSS?
  GoogleString head_markup = StrCat("<style>", CSS_console_css, "</style>\n");
  AdminHtml admin_html("console", head_markup, source, timer_, fetch,
                       message_handler_);
  if (statistics_enabled && logging_enabled && log_dir_set) {
    fetch->Write(
        "<div class='console_div' id='suggestions'>\n"
        "  <div class='console_div' id='pagespeed-graphs-container'>"
        "</div>\n</div>\n"
        "<script src='https://www.google.com/jsapi'></script>\n"
        "<script>var pagespeedStatisticsUrl = '';</script>\n"
        "<script>",
        handler);
    // From the admin page, the console JSON is relative, so it can
    // be set to ''.  Formerly it was set to options.statistics_handler_path(),
    // but there does not appear to be a disadvantage to always handling it
    // from whatever URL served this console HTML.
    //
    // TODO(jmarantz): Change the JS to remove pagespeedStatisticsUrl.
    fetch->Write(console_js, handler);
    fetch->Write("google.setOnLoadCallback(pagespeed.startConsole);", handler);
    fetch->Write("</script>\n", handler);
  } else {
    fetch->Write(
        "<p>\n"
        "  Failed to load PageSpeed Console because:\n"
        "</p>\n"
        "<ul>\n",
        handler);
    if (!statistics_enabled) {
      fetch->Write("  <li>Statistics is not enabled.</li>\n", handler);
    }
    if (!logging_enabled) {
      fetch->Write(
          "  <li>StatisticsLogging is not enabled."
          "</li>\n",
          handler);
    }
    if (!log_dir_set) {
      fetch->Write("  <li>LogDir is not set.</li>\n", handler);
    }
    fetch->Write(
        "</ul>\n"
        "<p>\n"
        "  In order to use the console you must configure these\n"
        "  options. See the <a href='https://developers.google.com/"
        "speed/pagespeed/module/console'>console documentation</a>\n"
        "  for more details.\n"
        "</p>\n",
        handler);
  }
}

void AdminSite::StatisticsJsonHandler(AsyncFetch* fetch, Statistics* stats) {
  fetch->response_headers()->SetStatusAndReason(HttpStatus::kOK);
  fetch->response_headers()->Add(HttpAttributes::kContentType,
                                 kContentTypeJson.mime_type());
  stats->DumpJson(fetch, message_handler_);
  fetch->Done(true);
}

void AdminSite::StatisticsHandler(const RewriteOptions& options,
                                  AdminSource source, AsyncFetch* fetch,
                                  Statistics* stats) {
  GoogleString head_markup =
      StrCat("<style>", CSS_statistics_css, "</style>\n");
  AdminHtml admin_html("statistics", head_markup, source, timer_, fetch,
                       message_handler_);
  // We have to call Dump() here to write data to sources for
  // system/system_test.sh: Line 79. We use JS to update the data in refreshes.
  fetch->Write("<pre id='stat'>", message_handler_);
  stats->Dump(fetch, message_handler_);
  fetch->Write("</pre>\n", message_handler_);
  StringPiece statistics_js = options.Enabled(RewriteOptions::kDebug)
                                  ? JS_statistics_js
                                  : JS_statistics_js_opt;
  fetch->Write(StrCat("<script type='text/javascript'>", statistics_js,
                      "\npagespeed.Statistics.Start();</script>\n"),
               message_handler_);
}

void AdminSite::GraphsHandler(const RewriteOptions& options, AdminSource source,
                              const QueryParams& query_params,
                              AsyncFetch* fetch, Statistics* statistics) {
  if (query_params.Has("json")) {
    ConsoleJsonHandler(query_params, fetch, statistics);
    return;
  }
  GoogleString head_markup = StrCat("<style>", CSS_graphs_css, "</style>\n");
  AdminHtml admin_html("graphs", head_markup, source, timer_, fetch,
                       message_handler_);
  fetch->Write(
      "<div id='cache_applied'>Loading Charts...</div>"
      "<div id='cache_type'>Loading Charts...</div>"
      "<div id='ipro'>Loading Charts...</div>"
      "<div id='image_rewriting'>Loading Charts...</div>"
      "<div id='realtime'>Loading Charts...</div>",
      message_handler_);
  fetch->Write(
      "<script type='text/javascript' "
      "src='https://www.google.com/jsapi'></script>",
      message_handler_);
  StringPiece graphs_js =
      options.Enabled(RewriteOptions::kDebug) ? JS_graphs_js : JS_graphs_js_opt;
  fetch->Write(StrCat("<script type='text/javascript'>", graphs_js,
                      "\npagespeed.Graphs.Start();</script>\n"),
               message_handler_);
}

void AdminSite::ConsoleJsonHandler(const QueryParams& params, AsyncFetch* fetch,
                                   Statistics* statistics) {
  StatisticsLogger* console_logger = statistics->console_logger();
  if (console_logger == nullptr) {
    fetch->response_headers()->SetStatusAndReason(HttpStatus::kNotFound);
    fetch->response_headers()->Add(HttpAttributes::kContentType, "text/plain");
    fetch->Write(
        "console_logger must be enabled to use '?json' query parameter.",
        message_handler_);
  } else {
    fetch->response_headers()->SetStatusAndReason(HttpStatus::kOK);

    // TODO(morlovich): It would be more secure to do:
    //    Content-Type: application/javascript; charset=utf-8
    // instead of using a JSON one.  There are probably a few other
    // anti-sniffing headers we could add as well.
    //
    // Also, it would be good to start what we serve with )]}' and a newline,
    // and updating the client js, to make completely sure the browser can't be
    // tricked into running it.
    fetch->response_headers()->Add(HttpAttributes::kContentType,
                                   kContentTypeJson.mime_type());

    std::set<GoogleString> var_titles;
    // Default is to fetch data used on graphs page.
    bool dump_for_graphs = true;

    // Default values for start_time, end_time, and granularity_ms in case the
    // query does not include these parameters.
    int64 start_time = 0;
    int64 end_time = timer_->NowMs();
    // Granularity is the difference in ms between data points. If it is not
    // specified by the query, the default value is 3000 ms, the same as the
    // default logging granularity.
    int64 granularity_ms = 3000;
    for (int i = 0; i < params.size(); ++i) {
      GoogleString value;
      if (params.UnescapedValue(i, &value)) {
        StringPiece name = params.name(i);
        if (name == "start_time") {
          StringToInt64(value, &start_time);
        } else if (name == "end_time") {
          StringToInt64(value, &end_time);
        } else if (name == "var_titles") {
          // Fetch specified data if users populate var_titles.
          dump_for_graphs = false;
          std::vector<StringPiece> variable_names;
          SplitStringPieceToVector(value, ",", &variable_names, true);
          for (size_t i = 0; i < variable_names.size(); ++i) {
            var_titles.insert(variable_names[i].as_string());
          }
        } else if (name == "granularity") {
          StringToInt64(value, &granularity_ms);
        }
      }
    }
    console_logger->DumpJSON(dump_for_graphs, var_titles, start_time, end_time,
                             granularity_ms, fetch, message_handler_);
  }
  fetch->Done(true);
}

void AdminSite::PrintHistograms(AdminSource source, AsyncFetch* fetch,
                                Statistics* stats) {
  AdminHtml admin_html("histograms", "", source, timer_, fetch,
                       message_handler_);
  stats->RenderHistograms(fetch, message_handler_);
}

namespace {

static const char kTableStart[] =
    "<table class='pagespeed-caches-structure'>\n"
    "  <thead>\n"
    "    <tr>\n"
    "      <td>Cache</td><td>Detail</td><td>Structure</td>\n"
    "    </tr>\n"
    "  </thead>\n"
    "  <tbody>";

static const char kTableEnd[] =
    "  </tbody>\n"
    "</table>";

// Takes a complicated descriptor like
//    "HTTPCache(Fallback(small=Batcher(cache=Stats(parefix=memcached_async,"
//    "cache=Async(AprMemCache)),parallelism=1,max=1000),large=Stats("
//    "prefix=file_cache,cache=FileCache)))"
// and strips away the crap most users don't want to see, as they most
// likely did not configure it, and return
//    "Async AprMemCache FileCache"
GoogleString HackCacheDescriptor(StringPiece name) {
  GoogleString out;
  // There's a lot of complicated syntax in the cache name giving the
  // detailed hierarchical structure.  This is really hard to read and
  // overly cryptic; it's designed for unit tests.  But let's extract
  // a few keywords out of this to understand the main pointers.
  static const char* kCacheKeywords[] = {
      "Compressed",  "Async",     "SharedMemCache", "LRUCache",
      "AprMemCache", "FileCache", "RedisCache"};
  const char* delim = "";
  for (int i = 0, n = arraysize(kCacheKeywords); i < n; ++i) {
    if (name.find(kCacheKeywords[i]) != StringPiece::npos) {
      StrAppend(&out, delim, kCacheKeywords[i]);
      delim = " ";
    }
  }
  if (out.empty()) {
    name.CopyToString(&out);
  }
  return out;
}

// Takes a complicated descriptor like
//    "HTTPCache(Fallback(small=Batcher(cache=Stats(prefix=memcached_async,"
//    "cache=Async(AprMemCache)),parallelism=1,max=1000),large=Stats("
//    "prefix=file_cache,cache=FileCache)))"
// and injects HTML line-breaks and indentation based on the parent depth,
// yielding HTML that renders like this (with &nbsp; and <br/>)
//    HTTPCache(
//       Fallback(
//          small=Batcher(
//             cache=Stats(
//                prefix=memcached_async,
//                cache=Async(
//                   AprMemCache)),
//             parallelism=1,
//             max=1000),
//          large=Stats(
//             prefix=file_cache,
//             cache=FileCache)))
GoogleString IndentCacheDescriptor(StringPiece name) {
  GoogleString out, buf;
  int depth = 0;
  for (int i = 0, n = name.size(); i < n; ++i) {
    StrAppend(&out, HtmlKeywords::Escape(name.substr(i, 1), &buf));
    switch (name[i]) {
      case '(':
        ++depth;
        FALLTHROUGH_INTENDED;
      case ',':
        out += "<br/>";
        for (int j = 0; j < depth; ++j) {
          out += "&nbsp; &nbsp;";
        }
        break;
      case ')':
        --depth;
        break;
    }
  }
  return out;
}

GoogleString CacheInfoHtmlSnippet(StringPiece label, StringPiece descriptor) {
  GoogleString out, escaped;
  StrAppend(&out, "<tr style='vertical-align:top;'><td>", label,
            "</td><td><input id='", label);
  StrAppend(&out, "_toggle' type='checkbox' ",
            "onclick=\"pagespeed.Caches.toggleDetail('", label,
            "')\"/></td><td><code id='", label, "_summary'>");
  StrAppend(&out,
            HtmlKeywords::Escape(HackCacheDescriptor(descriptor), &escaped));
  StrAppend(&out, "</code><code id='", label,
            "_detail' style='display:none;'>");
  StrAppend(&out, IndentCacheDescriptor(descriptor));
  StrAppend(&out, "</code></td></tr>\n");
  return out;
}

}  // namespace

void AdminSite::PrintCaches(
    bool is_global, AdminSource source, const GoogleUrl& stripped_gurl,
    const QueryParams& query_params, const RewriteOptions* options,
    SystemCachePath* cache_path, AsyncFetch* fetch, SystemCaches* system_caches,
    CacheInterface* filesystem_metadata_cache, HTTPCache* http_cache,
    CacheInterface* metadata_cache, PropertyCache* page_property_cache,
    ServerContext* server_context) {
  GoogleString url;
  if ((source == kPageSpeedAdmin) &&
      query_params.Lookup1Unescaped("url", &url)) {
    // Delegate to ShowCacheHandler to get the cached value for that
    // URL, which it may do asynchronously, so we cannot use the
    // AdminHtml abstraction which closes the connection in its
    // destructor.
    GoogleString json_dummy;
    ServerContext::Format format = ServerContext::kFormatAsHtml;
    if (query_params.Lookup1Unescaped("json", &json_dummy)) {
      format = ServerContext::kFormatAsJson;
    }
    GoogleString ua;
    query_params.Lookup1Unescaped("user_agent", &ua);
    server_context->ShowCacheHandler(
        format, url, ua, query_params.Has("Delete"), fetch, options->Clone());
  } else if ((source == kPageSpeedAdmin) &&
             query_params.Lookup1Unescaped("new_set", &url)) {
    ResponseHeaders* response_headers = fetch->response_headers();
    response_headers->SetStatusAndReason(HttpStatus::kOK);
    response_headers->Add(HttpAttributes::kContentType, "text/html");
    fetch->Write(options->PurgeSetString(), message_handler_);
    fetch->Done(true);
  } else if ((source == kPageSpeedAdmin) &&
             query_params.Lookup1Unescaped("purge", &url)) {
    ResponseHeaders* response_headers = fetch->response_headers();
    if (!options->enable_cache_purge()) {
      response_headers->SetStatusAndReason(HttpStatus::kOK);
      response_headers->Add(HttpAttributes::kContentType, "text/html");
      // TODO(jmarantz): virtualize the formatting of this message so that
      // it's correct in ngx_pagespeed and mod_pagespeed (and IISpeed etc).
      fetch->Write("Purging not enabled: please add\n", message_handler_);
      HtmlKeywords::WritePre(
          StrCat("  ", server_context->FormatOption("EnableCachePurge", "on")),
          "", fetch, message_handler_);
      fetch->Write(
          "\nto your configuration file. See the \n"
          "<a href='https://developers.google.com/speed/pagespeed/module"
          "/system#purge_cache'>documentation</a> "
          "for more information about cache purging.",
          message_handler_);
      fetch->Done(true);
    } else if (url == "*") {
      PurgeHandler(url, cache_path, fetch);
    } else if (url.empty()) {
      response_headers->SetStatusAndReason(HttpStatus::kOK);
      response_headers->Add(HttpAttributes::kContentType, "text/html");
      fetch->Write("Empty URL", message_handler_);
      fetch->Done(true);
    } else {
      GoogleUrl origin(stripped_gurl.Origin());
      GoogleUrl resolved(origin, url);
      if (!resolved.IsWebValid()) {
        response_headers->SetStatusAndReason(HttpStatus::kOK);
        response_headers->Add(HttpAttributes::kContentType, "text/html");
        GoogleString escaped_url;
        HtmlKeywords::Escape(url, &escaped_url);
        fetch->Write(StrCat("Invalid URL: ", escaped_url), message_handler_);
        fetch->Done(true);
      } else {
        PurgeHandler(resolved.Spec(), cache_path, fetch);
      }
    }
  } else {
    GoogleString head_markup = StrCat("<style>", CSS_caches_css, "</style>\n");
    AdminHtml admin_html("cache", head_markup, source, timer_, fetch,
                         message_handler_);

    fetch->Write("<div id='show_metadata'>", message_handler_);
    // Present a small form to enter a URL.
    if (source == kPageSpeedAdmin) {
      const char* user_agent =
          fetch->request_headers()->Lookup1(HttpAttributes::kUserAgent);
      fetch->Write(ServerContext::ShowCacheForm(user_agent), message_handler_);
    }
    fetch->Write("</div>\n", message_handler_);
    // Display configured cache information.
    if (system_caches != nullptr) {
      int flags = SystemCaches::kDefaultStatFlags;
      if (is_global) {
        flags |= SystemCaches::kGlobalView;
      }

      // TODO(jmarantz): Consider whether it makes sense to disable
      // either of these flags to limit the content when someone asks
      // for info about the cache.
      flags |= SystemCaches::kIncludeMemcached;
      flags |= SystemCaches::kIncludeRedis;
      fetch->Write("<div id='cache_struct'>", message_handler_);
      fetch->Write(kTableStart, message_handler_);
      CacheInterface* fsmdc = filesystem_metadata_cache;
      fetch->Write(
          StrCat(CacheInfoHtmlSnippet("HTTP Cache", http_cache->Name()),
                 CacheInfoHtmlSnippet("Metadata Cache", metadata_cache->Name()),
                 CacheInfoHtmlSnippet(
                     "Property Cache",
                     page_property_cache->property_store()->Name()),
                 CacheInfoHtmlSnippet(
                     "FileSystem Metadata Cache",
                     (fsmdc == nullptr) ? "none" : fsmdc->Name())),
          message_handler_);
      fetch->Write(kTableEnd, message_handler_);
      fetch->Write("</div>", message_handler_);

      fetch->Write("<div id='physical_cache'>", message_handler_);
      GoogleString backend_stats;
      system_caches->PrintCacheStats(
          static_cast<SystemCaches::StatFlags>(flags), &backend_stats);
      if (!backend_stats.empty()) {
        HtmlKeywords::WritePre(backend_stats, "", fetch, message_handler_);
      }
      fetch->Write("</div>", message_handler_);

      fetch->Write("<div id='purge_cache'>", message_handler_);
      // Filled in by JS in caches.js: updatePurgeSet().
      fetch->Write(
          "<h3>Purge Set</h3>"
          "<div id='purge_global'"
          " class='pagespeed-caches-purge-global'></div>"
          "<div id='purge_table'"
          " class='pagespeed-caches-purge-table'></div>"
          "</div>",  // closes 'purge_cache'.
          message_handler_);
    }
    StringPiece caches_js = options->Enabled(RewriteOptions::kDebug)
                                ? JS_caches_js
                                : JS_caches_js_opt;
    // Practice what we preach: put the blocking JS in the tail.
    // TODO(jmarantz): use static asset manager to compile & deliver JS
    // externally.
    fetch->Write(StrCat("<script type='text/javascript'>", caches_js,
                        "\npagespeed.Caches.Start();</script>\n"),
                 message_handler_);
  }
}

void AdminSite::PrintConfig(
    AdminSource source, AsyncFetch* fetch,
    SystemRewriteOptions* global_system_rewrite_options) {
  AdminHtml admin_html("config", "", source, timer_, fetch, message_handler_);
  HtmlKeywords::WritePre(global_system_rewrite_options->OptionsToString(), "",
                         fetch, message_handler_);
}

void AdminSite::MessageHistoryHandler(const RewriteOptions& options,
                                      AdminSource source, AsyncFetch* fetch) {
  // Request for page /mod_pagespeed_message.
  GoogleString log;
  StringWriter log_writer(&log);
  AdminHtml admin_html("message_history", "", source, timer_, fetch,
                       message_handler_);
  if (message_handler_->Dump(&log_writer)) {
    fetch->Write("<div id='log'>", message_handler_);
    // Write pre-tag and color messages.
    StringPieceVector messages;
    message_handler_->ParseMessageDumpIntoMessages(log, &messages);
    for (int i = 0, size = messages.size(); i < size; ++i) {
      if (messages[i].length() > 0) {
        switch (message_handler_->GetMessageType(messages[i])) {
          case kError: {
            HtmlKeywords::WritePre(
                message_handler_->ReformatMessage(messages[i]),
                "color:red; margin:0;", fetch, message_handler_);
            break;
          }
          case kWarning: {
            HtmlKeywords::WritePre(
                message_handler_->ReformatMessage(messages[i]),
                "color:brown; margin:0;", fetch, message_handler_);
            break;
          }
          case kFatal: {
            HtmlKeywords::WritePre(
                message_handler_->ReformatMessage(messages[i]),
                "color:orange; margin:0;", fetch, message_handler_);
            break;
          }
          default: {
            HtmlKeywords::WritePre(
                message_handler_->ReformatMessage(messages[i]), "margin:0;",
                fetch, message_handler_);
          }
        }
      }
    }
    fetch->Write("</div>\n", message_handler_);
    StringPiece messages_js = options.Enabled(RewriteOptions::kDebug)
                                  ? JS_messages_js
                                  : JS_messages_js_opt;
    fetch->Write(StrCat("<script type='text/javascript'>", messages_js,
                        "\npagespeed.Messages.Start();</script>\n"),
                 message_handler_);
  } else {
    fetch->Write(
        "<p>Writing to mod_pagespeed_message failed. \n"
        "Verify that MessageBufferSize is not set to 0 "
        "in pagespeed.conf.</p>\n",
        message_handler_);
  }
}

void AdminSite::AdminPage(
    bool is_global, const GoogleUrl& stripped_gurl,
    const QueryParams& query_params, const RewriteOptions* options,
    SystemCachePath* cache_path, AsyncFetch* fetch, SystemCaches* system_caches,
    CacheInterface* filesystem_metadata_cache, HTTPCache* http_cache,
    CacheInterface* metadata_cache, PropertyCache* page_property_cache,
    ServerContext* server_context, Statistics* statistics, Statistics* stats,
    SystemRewriteOptions* global_system_rewrite_options) {
  // The handler is "pagespeed_admin", so we must dispatch off of
  // the remainder of the URL.  For
  // "http://example.com/pagespeed_admin/foo?a=b" we want to pull out
  // "foo".
  //
  // Note that the comments here referring to "/pagespeed_admin" reflect
  // only the default admin path in Apache for fresh installs.  In fact
  // we can put the handler on any path, and this code should still work;
  // all the paths here are specified relative to the incoming URL.
  StringPiece path = stripped_gurl.PathSansQuery();  // "/pagespeed_admin/foo"
  path = path.substr(1);                             // "pagespeed_admin/foo"

  // If there are no slashes at all in the path, e.g. it's "pagespeed_admin",
  // then the relative references to "config" etc will not work.  We need
  // to serve the admin pages on "/pagespeed_admin/".  So if we got to this
  // point and there are no slashes, then we can just redirect immediately
  // by adding a slash.
  //
  // If the user has mapped the pagespeed_admin handler to a path with
  // an embedded slash, say "pagespeed/myadmin", then it's hard to tell
  // whether we should redirect, because we don't know what the the
  // intended path is.  In this case, we'll fall through to a leaf
  // analysis on "myadmin", fail to find a match, and print a "Did You Mean"
  // page.  It's not as good as a redirect but since we can't tell an
  // omitted slash from a typo it's the best we can do.
  if (path.find('/') == StringPiece::npos) {
    // If the URL is "/pagespeed_admin", then redirect to "/pagespeed_admin/" so
    // that relative URL references will work.
    ResponseHeaders* response_headers = fetch->response_headers();
    response_headers->SetStatusAndReason(HttpStatus::kMovedPermanently);
    GoogleString admin_with_slash = StrCat(stripped_gurl.AllExceptQuery(), "/");
    response_headers->Add(HttpAttributes::kLocation, admin_with_slash);
    response_headers->Add(HttpAttributes::kContentType, "text/html");
    GoogleString escaped_url;
    HtmlKeywords::Escape(admin_with_slash, &escaped_url);
    fetch->Write(StrCat("Redirecting to URL ", escaped_url), message_handler_);
    fetch->Done(true);
  } else {
    StringPiece leaf = stripped_gurl.LeafSansQuery();
    if ((leaf == "statistics") || (leaf.empty())) {
      StatisticsHandler(*options, kPageSpeedAdmin, fetch, stats);
    } else if (leaf == "stats_json") {
      StatisticsJsonHandler(fetch, stats);
    } else if (leaf == "graphs") {
      GraphsHandler(*options, kPageSpeedAdmin, query_params, fetch, statistics);
    } else if (leaf == "config") {
      PrintConfig(kPageSpeedAdmin, fetch, global_system_rewrite_options);
    } else if (leaf == "console") {
      // TODO(jmarantz): add vhost-local and aggregate message buffers.
      ConsoleHandler(*global_system_rewrite_options, *options, kPageSpeedAdmin,
                     query_params, fetch, statistics);
    } else if (leaf == "message_history") {
      MessageHistoryHandler(*options, kPageSpeedAdmin, fetch);
    } else if (leaf == "cache") {
      PrintCaches(is_global, kPageSpeedAdmin, stripped_gurl, query_params,
                  options, cache_path, fetch, system_caches,
                  filesystem_metadata_cache, http_cache, metadata_cache,
                  page_property_cache, server_context);
    } else if (leaf == "histograms") {
      PrintHistograms(kPageSpeedAdmin, fetch, stats);
    } else {
      fetch->response_headers()->SetStatusAndReason(HttpStatus::kNotFound);
      fetch->response_headers()->Add(HttpAttributes::kContentType, "text/html");
      fetch->Write("Unknown admin page: ", message_handler_);
      HtmlKeywords::WritePre(leaf, "", fetch, message_handler_);

      // It's possible that the handler is installed on /a/b/c, and we
      // are now reporting "unknown admin page: c".  This is kind of a guess,
      // but provide a nice link here to what might be the correct admin page.
      //
      // This is just a guess, so we don't want to redirect.
      fetch->Write("<br/>Did you mean to visit: ", message_handler_);
      GoogleString escaped_url;
      HtmlKeywords::Escape(StrCat(stripped_gurl.AllExceptQuery(), "/"),
                           &escaped_url);
      fetch->Write(
          StrCat("<a href='", escaped_url, "'>", escaped_url, "</a>\n"),
          message_handler_);
      fetch->Done(true);
    }
  }
}

void AdminSite::StatisticsPage(
    bool is_global, const QueryParams& query_params,
    const RewriteOptions* options, AsyncFetch* fetch,
    SystemCaches* system_caches, CacheInterface* filesystem_metadata_cache,
    HTTPCache* http_cache, CacheInterface* metadata_cache,
    PropertyCache* page_property_cache, ServerContext* server_context,
    Statistics* statistics, Statistics* stats,
    SystemRewriteOptions* global_system_rewrite_options) {
  if (query_params.Has("json")) {
    ConsoleJsonHandler(query_params, fetch, statistics);
  } else if (query_params.Has("config")) {
    PrintConfig(kStatistics, fetch, global_system_rewrite_options);
  } else if (query_params.Has("histograms")) {
    PrintHistograms(kStatistics, fetch, stats);
  } else if (query_params.Has("graphs")) {
    GraphsHandler(*options, kStatistics, query_params, fetch, statistics);
  } else if (query_params.Has("cache")) {
    GoogleUrl empty_url;
    PrintCaches(is_global, kStatistics, empty_url, query_params, options,
                nullptr,  // cache_path is reference from statistics page.
                fetch, system_caches, filesystem_metadata_cache, http_cache,
                metadata_cache, page_property_cache, server_context);
  } else {
    StatisticsHandler(*options, kStatistics, fetch, stats);
  }
}

namespace {

// Provides a Done(bool, StringPiece) entry point for use as a Purge
// callback. Translates the success into an Http status code for the
// AsyncFetch, sending any failure reason in the response body.
class PurgeFetchCallbackGasket {
 public:
  PurgeFetchCallbackGasket(AsyncFetch* fetch, MessageHandler* handler)
      : fetch_(fetch), message_handler_(handler) {}
  void Done(bool success, StringPiece reason) {
    ResponseHeaders* headers = fetch_->response_headers();
    headers->set_status_code(HttpStatus::kOK);
    headers->Add(HttpAttributes::kContentType, "text/html");
    // TODO(xqyin): Currently we may still return 'purge successful' even if
    // the URL does not exist in our cache. Figure out how to solve this case
    // while we don't want to search the whole cache which could be very large.
    if (success) {
      fetch_->Write("Purge successful", message_handler_);
    } else {
      GoogleString buf;
      fetch_->Write(HtmlKeywords::Escape(reason, &buf), message_handler_);
      fetch_->Write("\n", message_handler_);
      fetch_->Write(HtmlKeywords::Escape(error_, &buf), message_handler_);
    }
    fetch_->Done(true);
    delete this;
  }

  void set_error(StringPiece x) { x.CopyToString(&error_); }

 private:
  AsyncFetch* fetch_;
  MessageHandler* message_handler_;
  GoogleString error_;

  DISALLOW_COPY_AND_ASSIGN(PurgeFetchCallbackGasket);
};

}  // namespace

void AdminSite::PurgeHandler(StringPiece url, SystemCachePath* cache_path,
                             AsyncFetch* fetch) {
  PurgeContext* purge_context = cache_path->purge_context();
  int64 now_ms = timer_->NowMs();
  PurgeFetchCallbackGasket* gasket =
      new PurgeFetchCallbackGasket(fetch, message_handler_);
  PurgeContext::PurgeCallback* callback =
      NewCallback(gasket, &PurgeFetchCallbackGasket::Done);
  if (strings::EndsWith(url, "*")) {
    // If the url is "*" we'll just purge everything.  Note that we will
    // ignore any sub-paths in the expression.  We can only purge the
    // entire cache, or specific URLs, not general wildcards.
    purge_context->SetCachePurgeGlobalTimestampMs(now_ms, callback);
  } else {
    purge_context->AddPurgeUrl(url, now_ms, callback);
  }
}

}  // namespace net_instaweb
